package com.opennews.openplatform.security

import com.opennews.openplatform.RequestMap
import com.opennews.openplatform.RequestMapService
import com.opennews.openplatform.SharedUtil
import com.opennews.openplatform.UserActiveTokenService
import com.opennews.openplatform.constants.HttpHeaderConstant
import com.opennews.openplatform.constants.ServerCodeConstant
import com.opennews.openplatform.constants.ServerError
import groovy.transform.CompileStatic
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@CompileStatic
class AuthRequestValidator {
    @Value('${jwt.signingKey}')
    String signingKey

    RequestMapService requestMapService
    TokenManager tokenManager
    UserActiveTokenManager userActiveTokenManager

    boolean checkAccessPermission(HttpServletRequest request, HttpServletResponse response) {
        // FullPath returns a matched route full path.
        // 404 returns an empty string.
        def urlFullPath = request.getRequestURI().trim()
        def granted = false

        if (!SharedUtil.isNullOrEmpty(urlFullPath) && !urlFullPath.toLowerCase().endsWith("error")) {
            // Gets authorization from request header.
            // The string should be like below example:
            // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
            def authorizationHeader = request.getHeader(HttpHeaderConstant.HTTP_HEADER_AUTHORIZATION)?.trim()

            // Validates token string to see if it is a valid access token and it does not expire yet.
            def parsedToken = checkAuthorizationHeader(authorizationHeader)

            // If token is valid we still need to check if it is still active.
            if (parsedToken.valid) {
                parsedToken.valid = userActiveTokenManager.isActiveAccessToken(parsedToken.claims.username, parsedToken.token)
            }

            // Whatever token is valid or not we still need to check on url access permission.
            granted = checkUrlOnRequestMap(request.method, request.contextPath, urlFullPath, parsedToken.claims)

            // If user was authenticated but token is no longer valid or user is not granted for current url 401 code will be sent.
            if ((parsedToken.claims != null && !parsedToken.valid) || !granted) {
                granted = false
                SharedUtil.sendErrorResponse(response, HttpStatus.UNAUTHORIZED.value(), ServerCodeConstant.UNAUTHORIZED, ServerError.ERROR_USER_ACCOUNT_NOT_AUTHORIZED, urlFullPath)
            }
        }

        return granted
    }

    /**
     * Checks on authorization header and tris to extract the claims.
     * @param authorizationHeader: String of authorization header. Example: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
     */
    ParsedToken checkAuthorizationHeader(String authorizationHeader) {
        def result = new ParsedToken()

        if (!SharedUtil.isNullOrEmpty(authorizationHeader)) {
            // Fields splits the string authorizationHeader around each instance of one or more consecutive white space characters.
            def fields = authorizationHeader.split("\\s")

            // Checks the string if it contains bearer and token.
            if (fields.length == 2) {
                // Gets token string.
                def token = fields[1]

                // Validates token string to see if it is a valid access token and it does not expire yet.
                result = tokenManager.validateToken(token, signingKey)
                result.token = token
            }
        }

        return result
    }

    /**
     * Checks requested url on security config request map.
     * @param requestMethod: String of http request method. Such like get, post.
     * @param contextPath: String of url root. Example: /web-api
     * @param url: String of full url. Example: /web-api/user/get
     * @param claims: Claims data.
     * @return True means request granted. False means request denied.
     */
    boolean checkUrlOnRequestMap(String requestMethod, String contextPath, String url, MyClaims claims) {
        String urlWithoutRoot
        String configuredUrl
        def granted = false

        // Removes root url.
        // For example, url is /web-api/user/get.
        // Url without root is /user/get.
        urlWithoutRoot = url.replace(contextPath, "")

        String tempUrl

        for (RequestMap item in RequestMapManager.getCachedRequestMaps()) {
            // If http request method is configured and it does not match then just ignore.
            if (!SharedUtil.isNullOrEmpty(item.httpMethod) && requestMethod.toUpperCase() != item.httpMethod.toString().toUpperCase()) {
                continue
            }

            // First of all we need to remove trailing wildcard.
            // For example, item.Url is /user/** or /user/*.
            // configuredUrl is /user.
            configuredUrl = item.url.replace("/**", "")
            configuredUrl = configuredUrl.replace("/*", "")

            // Removes the configuredUrl from urlWithoutRoot.
            // Before removing, /user/get.
            // After removing, /get.
            tempUrl = urlWithoutRoot.replace(configuredUrl, "")

            if (tempUrl == "") {
                // If tempUrl is empty that means the url is exactly matched with configuredUrl.
                // Then we need to check on item.ConfigAttribute.
                granted = checkUrlOnConfigAttribute(claims, item.configAttribute)

                // If we find exactly match we just need to stop.
                break
            }

            if (tempUrl == urlWithoutRoot) {
                // If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
                // That means a mismatch. We need to check on next.
                continue
            }

            if (!tempUrl.startsWith("/")) {
                // If tempUrl does NOT start with "/" that means the parent url does not fully match.
                // For example
                // configuredUrl: /measuringData
                // urlWithoutRoot: /measuringDataSharing/queryValidDetails
                // tempUrl will be Sharing/queryValidDetails
                continue
            }

            // Checks if url starts with tempUrl.
            // For example /authentication is configured but NOT /api/authentication.
            // We only check wildcard config if url ends with tempUrl.
            if (!urlWithoutRoot.startsWith(tempUrl)) {
                if (item.url.endsWith(RequestMapManager.URL_DOUBLE_WILDCARD)) {
                    // If tempUrl is /get or /status/check it would also match the double wildcard.
                    // Then we need to check on item.ConfigAttribute.
                    granted = checkUrlOnConfigAttribute(claims, item.configAttribute)
                } else if (item.url.endsWith(RequestMapManager.URL_SINGLE_WILDCARD)) {
                    // If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
                    // Then we need to check on item.ConfigAttribute.
                    if (tempUrl.count("/") == 1) {
                        granted = checkUrlOnConfigAttribute(claims, item.configAttribute)
                    }
                }
            }
        }

        return granted
    }

    /**
     * Checks requested url access permission on security configAttribute.
     * This would check on authentication and authorization.
     * @param claims
     * @param configAttribute
     * @return
     */
    private boolean checkUrlOnConfigAttribute(MyClaims claims, String configAttribute) {
        def granted = false

        if (configAttribute.startsWith(RequestMapManager.DENY_ALL)) {
            granted = false
        } else if (configAttribute.startsWith(RequestMapManager.PERMIT_ALL)) {
            granted = true
        } else if (configAttribute.startsWith(RequestMapManager.IS_AUTHENTICATED_PREFIX) && claims != null) {
            granted = true
        } else if ((configAttribute.startsWith(RequestMapManager.HAS_ROLE_PREFIX) ||
            configAttribute.startsWith(RequestMapManager.HAS_ANY_ROLE_PREFIX)) && claims != null) {
            granted = requestMapService.containsAnyRoleInConfigAttribute(claims.roles, configAttribute)
        } else if ((configAttribute.startsWith(RequestMapManager.HAS_NO_ROLE_PREFIX) ||
            configAttribute.startsWith(RequestMapManager.HAS_NO_ANY_ROLE_PREFIX)) && claims != null) {
            granted = !requestMapService.containsAnyRoleInConfigAttribute(claims.roles, configAttribute)
        }

        return granted
    }
}